#! /usr/bin/env python

#make executable with: chmod +x pyBass.py

from Tkinter import *
import tkFont
import time
import threading
import math
import bisect
import midiBuffer
import mingus.core.chords as chords # requires mingus http://code.google.com/p/mingus/
from mingus.containers.Note import Note
from mingus.containers.NoteContainer import NoteContainer
import random
from pyswip import Prolog # requires pyswip http://code.google.com/p/pyswip/
import os
from PIL import Image # requires http://www.pythonware.com/products/pil/, i got it from macports py26-pil
from PIL import ImageTk
from PIL import ImageChops

#note i had to add the following to .bash_profile:
## #so my pre-macports python2.6 install can see py26-pil that i installed with macports                                                                     
## export PYTHONPATH=/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/

from string import *

def cumsum(x):
	return reduce(lambda a,b: a+[a[len(a)-1]+b],x[1:],[x[0]])	

class Player():
	def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
		self.midiBuffer=midiBuff
		self.midiBuffer.programChange(prog,chan)
		self.chan=chan
		self.changes=changes
		self.subDivs=subDivs
		self.beatsPerMeasure=beatsPerMeasure
		self.lastNotes=set([])
		self.notes=set([])
		self.lo=lo
		self.hi=hi

	def doBeat(self,beat,subDiv,dur):
		self.chooseNotes(beat,subDiv)
		self.notes=set(self.notes)
		self.midiBuffer.notesOff(self.lastNotes-self.notes,self.chan)
		self.midiBuffer.notesOn(self.notes-self.lastNotes,self.vel,self.chan)
		self.lastNotes=self.notes

	def reset(self):
		self.midiBuffer.notesOff(self.lastNotes,self.chan)
		self.lastNotes=set([])
		self.notes=set([])

	def newChord(self,beat):
		prev=beat-1
		if prev<0:
			prev=len(self.changes)-1
		return self.changes[beat]!=self.changes[prev]

	def getTones(self,beat):
		# print self.changes[beat]
		return self.changes[beat]['nums']

class Chords(Player):
	def __init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo=0,hi=midiBuffer.MIDI_MAX):
		Player.__init__(self,midiBuff,chan,prog,changes,beatsPerMeasure,subDivs,lo,hi)
		self.vel=90
	
	def chooseNotes(self,beat,subDiv):
		self.notes=self.getTones(beat)
		
class Application(Frame):
	def tempoC(self,val): pass
	
	def toggle(self):
		if self.blinker.running:
			self.blinker.running=False
			self.startB["text"] = "start"
			if self.cmdID != []:
				self.master.after_cancel(self.cmdID)
				self.cmdID=[]
			self.labels[self.pos]["bg"]="white"
		else:
			self.blinker.running=True
			self.startB["text"] = "stop"
			self.pos=0
			self.doBlink()
		
	def generate(self): pass
	
	def doBlink(self):
		#print ".",
		if self.blinker.state != self.state:
			self.state=self.blinker.state
			if self.pos!=self.blinker.newPos:
				self.labels[self.pos]["bg"]="white"
				self.pos=self.blinker.newPos
			if self.state:
				self.labels[self.pos]["bg"]="red"
				#self.bell()
			else:
				self.labels[self.pos]["bg"]="white"
		self.cmdID=self.master.after(10,self.doBlink)
	
	def doQuit(self):
		self.blinker.stop=True
		self.blinker.join()
		self.quit()
	
	def createWidgets(self):
		prolog = Prolog()
		prolog.consult('theory.pl')
		
		# adapted from mingus -- i need to rewrite to notate in chorale style
		def lilyNote(whiteKey, accidental, octave):
			out = whiteKey.lower()
			if accidental == '#':
				out += 'is'
			elif accidental == 'b':
				out += 'es'
			elif accidental == 'x':
				out += 'isis'
			elif accidental == 'bb':
				out += 'eses'
			octave-=2
			while octave != 0:
				if octave>0:
					out += "'"
					octave-=1
				else:
					out+=","
					octave+=1
			return out+'4'		
		
		# extend to use figured bass http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Figured-bass#Figured-bass
		def extractVoicings(voicings): # knows how to unpack X from a call to voiceLeadingOK(X)
			out=[]
			last=[]
			for v in voicings:
				ns=[]
				nv=len(v[1][0])
				lilyStrLoc=[]
				for ni in range(nv):
					n=v[1][0][ni]
					whiteKey= "%s" % n[0][0]
					accidental= "%s" % n[0][1]
					octave=  n[1]
					ns.append(whiteKey + accidental + "%d" % octave)
					lilyStrLoc.append('')
					if len(last)>0 and ns[ni]==last[ni]:
						lilyStrLoc[ni]+=' ~ '
					lilyStrLoc[ni]+=lilyNote(whiteKey,accidental,octave)+' '
				last=ns
					
				out.append({'nums':v[0],
						   'notes':ns,
						   'root':"%s%s" % (v[1][1][0],v[1][1][1]),
						   'quality':"%s" % v[1][2],
						   'size':"%s" % v[1][3],
						   'inversion':"%s" % v[1][4],
						   'lilyStr':lilyStrLoc})
			return out
		
		numVoicings=4
		varName='X'
		query="length(%s,%d),voiceLeadingOK(%s)" % (varName,numVoicings,varName)
		result=prolog.query(query) # calling non-exported predicates gives a PL_exception!  as long as result exists, other queries will fail!
		
		clefStr=['treble','bass']
		
		class InconsistentNumVoicesError(Exception): pass
		
		lilyStrFinal="\\version \"2.12.1\" "		
		
		lilyStr=[]
		numPhrases=2
		self.phrases=[]
		for p in range(numPhrases):
			self.phrases.append(extractVoicings(result.next()[varName]))
			lilyStr.append([])
			numVoices=4  #should count this instead of hardcoding
			for voiceNum in range(numVoices):
				lilyStr[p].append(' { ')
			for v in self.phrases[-1]:
				nv=len( v['lilyStr'] )
				if nv!=numVoices:
					raise InconsistentNumVoicesError
				for li in range(nv):
					lilyStr[p][li] += v['lilyStr'][li]
			for voiceNum in range(numVoices):
				lilyStr[p].append(' } ')
			thisPhrase = " \score { << "
			cn=len(clefStr)
			currVoice=numVoices-1
			for c in range(cn):
				thisClef = " \\new Staff { \clef " + clefStr[c] + " \key c \major \\time 4/4 << "
				firstTime=True
				voicesPerStaff=numVoices/cn
				for vNum in range(currVoice,currVoice-voicesPerStaff,-1):
					# print "doing line %d in phrase %d" % (vNum,p)
					if firstTime:
						firstTime=False					
					else:
						thisClef += " } \\\\ "
					thisClef += lilyStr[p][vNum]
				thisPhrase += thisClef + " } >> } "
				currVoice-=voicesPerStaff
			lilyStrFinal += thisPhrase + " >> \layout {} \midi {} } "
		
		filename = "lily"

		try:
			f = open(filename + ".ly", 'w')
			f.write(lilyStrFinal)
			f.close()
			os.system("lilypond --png -o " + filename + " " + filename + ".ly")
			os.remove(filename + ".ly")
			os.remove(filename + ".ps")
		except:
			print "error writing .ly file"
			
		im = Image.open(filename + ".png")	
		im = im.crop(ImageChops.invert(im).getbbox())
		self.score = ImageTk.PhotoImage(im)
		
		self.scoreL = Label(self, image=self.score)
		self.scoreL.grid()
		
		self.beatsPerMeasure=4
		self.swing=False
		self.state=False
		
		self.quitB = Button(self)
		self.quitB["text"] = "quit"
		self.quitB["command"] = self.doQuit
		self.quitB.grid()
		
		self.startB = Button(self)
		self.startB["text"] = "start"
		self.startB["command"] = self.toggle
		self.startB.grid()
		
		self.genB = Button(self)
		self.genB["text"] = "generate"
		self.genB["command"] = self.generate
		self.genB.grid()
		
		self.labels=[]
		self.labelStrs=[]
		self.beats=[]
		f=tkFont.Font(family="Courier",size=50,weight="bold")
		
		self.main=Frame(self)
		self.main.grid()
		self.subFrames=[]
		chords=[]
		for p in range(len(self.phrases)):
			phrase=self.phrases[p]
			for m in range(len(phrase)):
				measure=phrase[m]
				self.subFrames.append(Frame(self.main))
				self.subFrames[len(self.subFrames)-1].grid(row=p,column=m,sticky=W)
				if type(measure) in [str,dict]:
					measure=[measure]
					self.phrases[p][m]=[self.phrases[p][m]]
				for s in range(len(measure)):
					chord=self.phrases[p][m][s]
					if type(chord)==str :
						chordStr=chord
					elif type(chord)==dict :
						chordStr="%s %s" % (chord['root'],chord['quality'])
					
					self.labelStrs.append(StringVar())
					self.labelStrs[len(self.labelStrs)-1].set(chordStr)
					self.labels.append(Label(self.subFrames[len(self.subFrames)-1],textvariable=self.labelStrs[len(self.labelStrs)-1],font=f))
					self.labels[len(self.labels)-1].grid(row=0,column=s,padx=10)
					beats=float(self.beatsPerMeasure)/len(measure)
					if beats>self.beatsPerMeasure/len(measure):
						if s%2==0:
							beats=math.ceil(beats)
						else:
							beats=math.floor(beats)
					self.beats.append(int(beats))
					for bt in range(int(beats)):
						chords.append(chord)
		
		self.cumBeats=cumsum(self.beats)
		
		self.tempo = Scale(self, command=self.tempoC, from_=30, to=500, label="tempo", orient=HORIZONTAL, length=600)
		self.tempo.set(200)
		self.tempo.grid()
		
		class Blinker(threading.Thread):
			def __init__(self,tempo,beats,cumBeats,chords,beatsPerMeasure,subDivs):
				self.tempo=tempo
				self.running=False
				self.beats=beats
				self.cumBeats=cumBeats
				self.chords=chords
				self.beatsPerMeasure=beatsPerMeasure
				self.subDivs=subDivs
				threading.Thread.__init__(self)
			
			def run(self):
				b=midiBuffer.midiBuffer(verbose=True,device=[])
				
				players=[]
				players.append(Chords(b,1,0,self.chords,self.beatsPerMeasure,self.subDivs)) #piano chan 2
				
				self.stop=False
				while not self.stop:
					if self.running:
						x=time.time()
						if x>self.baseTime:
							if self.baseTime==0:
								self.baseTime=x
							dur=60.0/self.tempo.get()/self.subDivs
							self.baseTime+=dur
							
							self.subDiv+=1
							if self.subDiv>=self.subDivs:
								self.subDiv=0
								self.beat+=1
								if self.beat>=sum(self.beats):
									self.beat=0
							
							self.state=self.subDiv==0
							self.newPos=bisect.bisect(self.cumBeats,self.beat)
							
							for player in players:
								player.doBeat(self.beat,self.subDiv,1000*dur)
						
						elif self.baseTime-time.time()>.1:
							bed=time.time()
							dur=.9*(self.baseTime-bed)
							time.sleep(dur)
							act=time.time()-bed
							#print "slept for " + str(act) + " (" + str(100*act/dur) + "% accurate)"
					else:
						self.baseTime=0
						self.state=False
						self.beat=0
						self.subDiv=-1

						for player in players:
							player.reset()
				for player in players:
					player.reset()                                                
				b.close(True)
		
		if self.swing:
			subDivs=3
		else:
			subDivs=4
		self.blinker=Blinker(self.tempo,self.beats,self.cumBeats,chords,self.beatsPerMeasure,subDivs)
		self.blinker.start()
	
	def __init__(self, master=None):
		Frame.__init__(self, master)
		self.grid()
		self.createWidgets()
		self.master=master             

def main():        
	root = Tk()
	app = Application(master=root)
	app.master.title("pyTheory")
	app.master.lift() #why doesn't this work?
	app.mainloop()
	root.destroy()

main()